package au.edu.swin.bank.ejb.session.stateless;

import au.edu.swin.bank.persistence.entity.Account;
import au.edu.swin.bank.persistence.entity.BankTransaction;
import au.edu.swin.bank.persistence.entity.BankTransactionDTO;
import au.edu.swin.bank.persistence.entity.BankTransactionType;
import au.edu.swin.bank.persistence.entity.Bill;
import au.edu.swin.bank.persistence.entity.BillPayment;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import javax.ejb.EJB;
import javax.ejb.Local;
import javax.ejb.Remote;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;

/**
 *
 * @author Luke
 */
@Stateless
@Remote(TransactionRemote.class)
@Local(TransactionLocal.class)
public class TransactionBean implements TransactionRemote, TransactionLocal
{
    @PersistenceContext
    private EntityManager em;

    @EJB
    AccountLocal accountBean;

    @EJB
    CustomerLocal customerBean;

    private Integer custId;

    /**
     * Transfers funds between two accounts.
     * 
     * @param aAmount The amount to transfer
     * @param aSrcAccountId The id of the account to transfer from
     * @param aDestAccountId The id of the account to transfer to
     * @param aDescription The description of the transfer
     * @return True if the transfer succeeds, otherwise false
     */
    public boolean transferFunds(double aAmount, BigInteger aSrcAccountId, BigInteger aDestAccountId, String aDescription)
    {
        //load the account objects for the source and destination accounts
        Account srcAccount = accountBean.getAccount(aSrcAccountId);
        Account destAccount = accountBean.getAccount(aDestAccountId);

        //business rules validation:
        aAmount = ((double)Math.floor(aAmount * 100)) / 100;    //remove rounding errors (since currency is only two decimal places)
        if (srcAccount == null //check that the source account exists
            || destAccount == null //check that the destination account exists
            || !customerOwnsAccount(srcAccount) //check that the source account belongs to the customer doing the transfer
            || !customerOwnsAccount(destAccount) //check that the destination account belongs to the customer doing the transfer
            || srcAccount.getAccountId().equals(destAccount.getAccountId()) //check that the source account is not the destination account
            || aAmount <= 0 //check that the amount being paid is more than zero
            || srcAccount.getBalance() < aAmount //check that the source account has sufficient credit to make the payment
        )
        {
            return false;
        }
        
        //withdraw from the src account and deposit into the dest account
        //persist these changes to the db
        boolean transactionSuccessful = accountBean.withdraw(aAmount, srcAccount) && accountBean.deposit(aAmount, destAccount);
        //TODO: look at transaction management i.e. if deposit fails, withdraw must be rolled back!

        //if the transaction was successful, create and persist a new transaction to the db
        if (transactionSuccessful)
        {
            createandPersistTransaction(aAmount, srcAccount, destAccount, new BankTransactionType('T'), new BigInteger(String.valueOf(new Date().getTime())), aDescription);
            return true;
        }

        return false;
    }

    /**
     * Pays a specified amount of a bill.
     * 
     * @param aAmount The amount of the bill to pay
     * @param aSrcAccountId The id of the account to pay the bill from
     * @param aBillId The id of the bill to pay
     * @param aDescription The description of the bill payment
     * @return True if the bill payment succeeds, otherwise false
     */
    public boolean payBill(double aAmount, BigInteger aSrcAccountId, BigInteger aBillId, String aDescription)
    {
        //load the account objects for the source and destination accounts
        Account srcAccount = accountBean.getAccount(aSrcAccountId);
        Query query = em.createNamedQuery("Bill.findByBillid").setParameter("billid", aBillId);
        Bill bill = (Bill) query.getSingleResult();
        Account billerAccount = accountBean.getAccount(bill.getBiller().getAccount().getAccountId());
        
        //business rules validation:
        aAmount = ((double)Math.floor(aAmount * 100)) / 100;    //remove rounding errors (since currency is only two decimal places)
        double amountRemaining = ((double)Math.floor((bill.getAmount() - bill.getAmountPaid()) * 100)) / 100; //remove rounding errors (since currency is only two decimal places)
        if (srcAccount == null //check that the source account exists
            || billerAccount == null //check that the destination account exists
            || !customerOwnsAccount(srcAccount) //check that the source account belongs to the customer doing the payment
            || srcAccount.getAccountId().equals(billerAccount.getAccountId()) //check that the source account is not the biller account
            || aAmount <= 0 //check that the amount being paid is more than zero
            || aAmount > amountRemaining  //check that the amount being paid is less than the bill amount remaining
            || srcAccount.getBalance() < aAmount //check that the source account has sufficient credit to make the payment
        )
        {
            return false;
        }
        
        //withdraw from the src account and deposit into the dest account
        //persist these changes to the db
        boolean transactionSuccessful = accountBean.withdraw(aAmount, srcAccount) && accountBean.deposit(aAmount, billerAccount);
        //TODO: look at transaction management i.e. if deposit fails, withdraw must be rolled back!

        //if the transaction was successful, create and persist a new transaction to the db
        if (transactionSuccessful)
        {
            BigInteger paymentDate = new BigInteger(String.valueOf(new Date().getTime()));
            createandPersistTransaction(aAmount, srcAccount, billerAccount, new BankTransactionType('B'), paymentDate, aDescription);
            createAndPersistBillPayment(paymentDate, bill, srcAccount, aAmount);

            return true;
        }

        return false;
    }

    /**
     * Withdraws funds from an account
     * @param aAmount The amount to withdraw
     * @param aAccountId The ID of the account to withdraw from
     * @param aDescription The description of the transaction
     * @return True if the transaction succeeds, otherwise false
     */
    public boolean withdrawFunds(double aAmount, BigInteger aAccountId, String aDescription)
    {
        //Load the account object for the source and destination accounts
        Account srcAccount = accountBean.getAccount(aAccountId);
        Account destAccount = accountBean.getAccount(BigInteger.valueOf(1));

        //Check both accounts exist
        if ((srcAccount == null) || (destAccount == null)) {
            return false;
        }

        //Check the customer owns the source account
        if (!customerOwnsAccount(srcAccount))
        {
            return false;
        }

        //Withdraw from the src account and persist these changes to the db
        boolean transactionSuccessful = accountBean.withdraw(aAmount, srcAccount);
        
        //If the transaction was successful, create and persist a new transaction to the db
        if (transactionSuccessful)
        {
            createandPersistTransaction(aAmount, srcAccount, destAccount, new BankTransactionType('W'), new BigInteger(String.valueOf(new Date().getTime())), aDescription);
            return true;
        }
        else
            return false;
    }

    /**
     * Deposits funds into an account
     * @param aAmount The amount to deposit
     * @param aAccountId The ID of the account to deposit into
     * @param aDescription The description of the transaction
     * @return True if the transaction succeeds, otherwise false
     */
    public boolean depositFunds(double aAmount, BigInteger aAccountId, String aDescription)
    {
        //Load the account object for the source and destination accounts
        Account srcAccount = accountBean.getAccount(BigInteger.valueOf(1));
        Account destAccount = accountBean.getAccount(aAccountId);

        //Check both accounts exist
        if ((srcAccount == null) || (destAccount == null)) {
            return false;
        }

        //Deposit into the dest account and persist these changes to the db
        boolean transactionSuccessful = accountBean.deposit(aAmount, destAccount);

        //If the transaction was successful, create and persist a new transaction to the db
        if (transactionSuccessful)
        {
            createandPersistTransaction(aAmount, srcAccount, destAccount, new BankTransactionType('D'), new BigInteger(String.valueOf(new Date().getTime())), aDescription);
            return true;
        }
        else
            return false;
    }

    /**
     * Creates a transaction and persists it to the database
     * @param aAmount The amount of funds involved in the transaction
     * @param aSrcAccount The source account of the transaction
     * @param aDestAccount The destination account of the transaction
     * @param aTransType The type of the transaction, represented as a character
     * @param aDate The date of the transaction
     * @param aDescription The description of the transaction
     */
    private void createandPersistTransaction(double aAmount, Account aSrcAccount, Account aDestAccount, BankTransactionType aTransType, BigInteger aDate, String aDescription)
    {
        //create a transaction object
        BankTransaction transaction = new BankTransaction();

        //set the properties of the transaction
        transaction.setAmount(aAmount);
        transaction.setFrmaccountid(aSrcAccount.getAccountId());
        transaction.setRecaccountid(aDestAccount.getAccountId());
        transaction.setBankTransactionType(aTransType);
        transaction.setDate(aDate);
        transaction.setDescription(aDescription);

        //save it to the database
        persist(transaction);
    }

    /**
     * Creates a bill payment and persists it to the database
     * @param aDate the date of the bill payment
     * @param aBill the bill being paid
     * @param aAccount the account that the bill is being paid from
     * @param aAmount the amount being paid
     */
    private void createAndPersistBillPayment(BigInteger aDate, Bill aBill, Account aAccount, double aAmount)
    {
        //create a bill payment object
        BillPayment billPayment = new BillPayment(aDate, aBill.getBillId());

        //set the properties of the bill payment
        billPayment.setAmount(aAmount);
        billPayment.setAccount(aAccount);
        billPayment.setBill(aBill);

        //save it to the database
        persist(billPayment);

        //update the bill's amount paid
        aBill.setAmountPaid(aBill.getAmountPaid() + aAmount);
        persist(aBill);
    }

    /**
     * Verifies that the customer performing the transaction has appropriate
     * ownership privileges on the account.
     * @param aAccount The account to check
     * @return True if the customer has adequate privileges, otherwise false.
     */
    private boolean customerOwnsAccount(Account aAccount)
    {
        if (!customerBean.isStaff(custId))
        {
            if (!aAccount.getCustomer().getCustId().equals(custId) || aAccount.getRemoved() > 0)
            {
                return false;
            }
        }
        return true;
    }
    
    /**
     * Gets a list of transactions for the specified account between the specified dates
     * @param aAccountId The account to get the transactions for
     * @param aStartDate The start date of the transaction period
     * @param aEndDate The end date of the transaction period
     * @return A collection of transactions
     */
    public Collection<BankTransactionDTO> getTransactions(BigInteger aAccountId, BigInteger aStartDate, BigInteger aEndDate)
    {
        Account account = accountBean.getAccount(aAccountId);

        if (account == null) {
            return new ArrayList<BankTransactionDTO>();
        }
        if (!customerOwnsAccount(account)) {
            return new ArrayList<BankTransactionDTO>();
        }
        return accountBean.getTransactions(account, aStartDate, aEndDate);
    }
    
    public void setCustId(Integer aCustId)
    {
        custId = aCustId;
    }

    /**
     * Persists the object to the database.
     * @param object
     */
    public void persist(Object object)
    {
        em.persist(object);
    }
    
    /**
     * Forces an eviction of the data in the Bean's cache.
     */
    public void forceEvict(){
        em.getEntityManagerFactory().getCache().evictAll();
    }
}
